/* Definitions for IO routines (windows version).

   This file is part of khipu.

   khipu is free software: you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

#include <winsock2.h>
#include <windows.h>
#include <cstring>
#include <cstdio>

KP_DECLS_BEGIN

bool fhandle_valid_p (fhandle_t fh)
{
  return (fh != INVALID_HANDLE_VALUE);
}

fhandle_t llio_open (interpreter *interp, const char *path,
  const char *mode, int *flagp)
{
  DWORD acc = 0, disp = 0;
  int sflags = 0;
  bool trunc = false;
  
  switch (*mode++)
    {
      case 'r':
        acc = GENERIC_READ;
        disp = OPEN_EXISTING;
        sflags = STRM_READ;
        break;

      case 'w':
        acc = GENERIC_WRITE;
        disp = OPEN_ALWAYS;
        sflags = STRM_WRITE;
        trunc = true;
        break;

      case 'a':
        acc = GENERIC_WRITE | FILE_APPEND_DATA;
        disp = OPEN_ALWAYS;
        sflags = STRM_WRITE | STRM_APP;
        break;

      default:
        return (nullptr);
    }

  switch (*mode++)
    {
      case '+':
        acc = GENERIC_READ | GENERIC_WRITE;
        sflags |= STRM_READ | STRM_WRITE;
        break;
      
      case 'b':
        sflags |= STRM_BIN;
        break;
	   
      case '\0':
        --mode;
        break;
	   
      default:
        return (INVALID_HANDLE_VALUE);
    }

  if (*mode == 'b')
    {
      if (sflags & STRM_BIN)
        return (INVALID_HANDLE_VALUE);
	
      sflags |= STRM_BIN;
      ++mode;
    }

  if (*mode == 'c')
    {
      if (disp == OPEN_ALWAYS)
        return (INVALID_HANDLE_VALUE);
	  
      disp = OPEN_ALWAYS, ++mode;
    }
  else if (*mode == 'x')
    {
      if (disp == OPEN_EXISTING)
        return (INVALID_HANDLE_VALUE);
	
      disp = OPEN_EXISTING, ++mode;
    }

  if (*mode)
    return (INVALID_HANDLE_VALUE);

  if (flagp)
    *flagp = sflags;

  KP_TMARK (interp);
  size_t path_len = strlen (path) + 1;
  wchar_t *wp = (wchar_t *)KP_TALLOC (interp, path_len * 4);

  if (!MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS,
                            path, path_len, wp, path_len * 4))
    return (INVALID_HANDLE_VALUE);

  fhandle_t rv = CreateFileW (wp, acc, FILE_SHARE_READ | FILE_SHARE_WRITE |
                              FILE_SHARE_DELETE, 0, disp,
                              FILE_ATTRIBUTE_NORMAL, 0);

  if (rv == INVALID_HANDLE_VALUE)
    return (rv);
  else if (trunc && GetFileType (rv) == FILE_TYPE_DISK && !SetEndOfFile (rv))
    {
      CloseHandle (rv);
      rv = INVALID_HANDLE_VALUE;
    }

  return (rv);
}

struct io_wait_point : public wait_point
{
  fhandle_t fh;
  OVERLAPPED *ovp = nullptr;

  io_wait_point (interpreter *ip, fhandle_t f) : wait_point (ip), fh (f)
    {
    }

  void set (OVERLAPPED *p = nullptr)
    {
      this->ovp = p;
    }

  ~io_wait_point ()
    {
      if (this->ovp)
        CancelIoEx (this->fh, this->ovp);
    }
};

static inline int
read_err ()
{
  return (GetLastError () == ERROR_HANDLE_EOF ? 0 : -1);
}

static int 
std_readb (interpreter *interp, fhandle_t fh, char *dst, uint32_t nbytes)
{
  OVERLAPPED ov;
  memset (&ov, 0, sizeof (ov));
  ov.Offset = SetFilePointer (fh, 0, (LONG *)&ov.OffsetHigh, FILE_CURRENT);

  ResetEvent (ov.hEvent = interp->io_event);
  io_wait_point ig { interp, fh };
  DWORD nread;

  if (ReadFile (fh, dst, nbytes, &nread, &ov))
    return ((int)nread);
  else if (GetLastError () != ERROR_IO_PENDING)
    return (-1);

  while (true)
    switch (WaitForSingleObjectEx (ov.hEvent, INFINITE, true))
      {
        case WAIT_OBJECT_0:
          return (ov.Internal == ERROR_HANDLE_EOF ?
                  0 : (ov.Internal ? -1 : (int)ov.InternalHigh));

        case WAIT_IO_COMPLETION:
          ig.set (&ov);
          interp->handle_evs ();
          ig.set ();
          break;

        default:
          return (read_err ());
      }
}

static int
console_readb (interpreter *interp, fhandle_t fh, char *dst, uint32_t nbytes)
{
  for (wait_point g { interp } ; ; )
    switch (WaitForSingleObjectEx (fh, INFINITE, true))
      {
        case WAIT_OBJECT_0:
          {
            DWORD n = 0, nr = 0;
            GetNumberOfConsoleInputEvents (fh, &n);
            if (!n)
              continue;

            INPUT_RECORD ir;
            for (DWORD i = 0; i < n; ++i)
              {
                PeekConsoleInput (fh, &ir, 1, &nr);
                if (!nr)
                  ;
                else if (ir.EventType == KEY_EVENT)
                  return (ReadFile (fh, dst, nbytes, &nr, nullptr) ?
                          (int)nr : read_err ());
                else
                  ReadConsoleInput (fh, &ir, 1, &nr);
              }

            break;
          }

        case WAIT_IO_COMPLETION:
          interp->handle_evs ();
          break;

        default:
          return (read_err ());
      }
}

static int
pipe_readb (interpreter *interp, fhandle_t fh, char *dst, uint32_t nbytes)
{
  for (wait_point g { interp } ; ; )
    {
      DWORD n = 0, nr;
      if (!PeekNamedPipe (fh, nullptr, 0, nullptr, &n, nullptr))
        return (read_err ());
      else if (n)
        return (ReadFile (fh, dst, nbytes, &nr, nullptr) ?
                (int)nr : read_err ());
      else if (SleepEx (50, true) == WAIT_IO_COMPLETION)
        interp->handle_evs ();
    }
}

static inline bool
socket_p (fhandle_t fh)
{
  WSANETWORKEVENTS ev;
  ev.lNetworkEvents = 0xdeadbeef;
  WSAEnumNetworkEvents ((SOCKET)fh, nullptr, &ev);
  return (ev.lNetworkEvents != (long)0xdeadbeef);
}

int llio_readb (interpreter *interp, fhandle_t fh, char *dst, uint32_t nb)
{
  switch (GetFileType (fh))
    {
      case FILE_TYPE_CHAR:
        return (console_readb (interp, fh, dst, nb));

      case FILE_TYPE_PIPE:
        if (!socket_p (fh))
          return (pipe_readb (interp, fh, dst, nb));
        // fallthrough.

      case FILE_TYPE_DISK:
        return (std_readb (interp, fh, dst, nb));

      default:
        return (-1);
    }
}

int llio_readt (interpreter *interp, fhandle_t fh, char *dst, uint32_t nb)
{
  int rv = llio_readb (interp, fh, dst, nb - 1);
  if (rv <= 0)
    return (rv);

  char *inp = dst, *outp = dst;
  for (; inp != dst + rv - 1; ++inp)
    {
      int ch = *inp;
      if (ch == 13 && inp[1] == 10)
        {
          *outp++ = 10;
          ++inp;
        }
      else
        *outp++ = ch;
    }

  if (*inp == 13)
    { // Got a CR at the end - Peek for a LF.
      char b = 0;
      int tmp = llio_readb (interp, fh, &b, 1);

      if (tmp <= 0)
        *outp++ = 13;
      else if (b == 10)
        *outp++ = 10;
      else
        {
          *outp++ = 13;
          *outp++ = b;
        }
    }
  else
    *outp++ = *inp;

  return (outp - dst);
}

int llio_writeb (interpreter *interp, fhandle_t fh, const char *src, uint32_t nb)
{
  OVERLAPPED ov;
  memset (&ov, 0, sizeof (ov));
  ov.Offset = ov.OffsetHigh = 0xffffffff;

  ResetEvent (ov.hEvent = interp->io_event);
  io_wait_point ig { interp, fh };
  DWORD nwritten;

  if (WriteFile (fh, src, nb, &nwritten, &ov))
    return ((int)nwritten);
  else if (GetLastError () != ERROR_IO_PENDING)
    return (-1);

  while (true)
    switch (WaitForSingleObjectEx (ov.hEvent, INFINITE, true))
      {
        case WAIT_OBJECT_0:
          return (ov.Internal ? -1 : (int)ov.InternalHigh);

        case WAIT_IO_COMPLETION:
          ig.set (&ov);
          interp->handle_evs ();
          ig.set ();
          break;

        default:
          return (-1);
      }
}

int llio_writet (interpreter *interp, fhandle_t fh,
  const char *src, uint32_t nb)
{
  int ret = 0;

  while (nb)
    {
      const uint32_t TSIZE = 1023;
      char buf[TSIZE + 1];
      const char *next = src, *src_end = src + nb, *lf;
      char *pbuf = buf, *pend = buf + TSIZE;
      uint32_t next_size = min (TSIZE, nb);

      while (next_size > 0 &&
          (lf = (const char *)memchr (next, 10, next_size)))
        {
          uint32_t len = (uint32_t)(lf - next);
          memcpy (pbuf, next, len);
          pbuf[len] = 13, pbuf[len + 1] = 10;
          pbuf += len + 2;
          next += len + 1;
          next_size = min (src_end - next,
                           max ((ptrdiff_t)0, pend - pbuf));
        }

      if (next_size)
        {
          memcpy (pbuf, next, next_size);
          pbuf += next_size;
          next += next_size;
        }

      char *wbuf = buf;
      for (uint32_t nw = (uint32_t)(pbuf - buf) ; nw ; )
        {
          int tmp = llio_writeb (interp, fh, wbuf, nw);
          if (tmp <= 0)
            return (tmp);
          
          wbuf += tmp;
          nw -= tmp;
        }

      ret += next - src;
      src += ret;
      nb -= ret;
    }

  return (ret);
}

bool llio_seek (interpreter *, fhandle_t fh, int64_t& pos, int whence)
{
  LARGE_INTEGER ipos, opos;
  ipos.QuadPart = pos;

  if (!SetFilePointerEx (fh, ipos, &opos, whence))
    return (false);

  pos = opos.QuadPart;
  return (true);
}

bool llio_close (fhandle_t fh)
{
  return (CloseHandle (fh));
}

bool llio_fhstat (interpreter *interp, fhandle_t fh, fhandle_stat& out)
{
  BY_HANDLE_FILE_INFORMATION info;
  interp->begin_blocking ();
  auto rv = GetFileInformationByHandle (fh, &info);
  interp->end_blocking ();

  if (!rv)
    return (false);

#define set_time(dst, input)   \
  memcpy (&out.dst, &info.ft##input, sizeof (out.dst))
  
  set_time (atime, LastAccessTime);
  set_time (mtime, LastWriteTime);
  set_time (ctime, CreationTime);

#undef set_time

  out.nlink = info.nNumberOfLinks;
  out.attr = info.dwFileAttributes;
  out.serialno = info.dwVolumeSerialNumber;
  out.size = info.nFileSizeLow | ((uint64_t)info.nFileSizeHigh << 32);
  out.index = info.nFileIndexLow | ((uint64_t)info.nFileIndexHigh << 32);
  return (true);
}

typedef long (NTAPI *setfh_info_fn) (HANDLE, void *, void *, ULONG, int);
static setfh_info_fn setfh_info;

struct wio_status
{
  void *p[2];
};

bool llio_truncate (fhandle_t fh, uint64_t size)
{   
  wio_status status;
  return (setfh_info (fh, &status, &size, sizeof (size), 20) >= 0);
}

object llio_fhpath (interpreter *interp, const char *path)
{
  wchar_t *wpath, *outp;
  KP_TMARK (interp);
  size_t len = strlen (path) + 1;

  wpath = (wchar_t *)KP_TALLOC (interp, (len + 1) * 2 * sizeof (wchar_t));
  if (!MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS,
                            path, len, wpath, len * 2 + 1))
    return (UNBOUND);

  while (true)
    {
      wchar_t tmp;
      size_t l2 = GetFullPathNameW (wpath, 1, &tmp, nullptr);

      if (!l2)
        return (UNBOUND);

      outp = (wchar_t *)KP_TALLOC (interp,
                                   (l2 + 1) * sizeof (*outp));
      len = GetFullPathNameW (wpath, len, outp, 0);
      if (!len)
        return (UNBOUND);
      else if (len <= l2)
        break;
    }

  char *ret = (char *)KP_TALLOC (interp, len * 4 + 1); 
  if (!WideCharToMultiByte (CP_UTF8, WC_ERR_INVALID_CHARS,
                            outp, len, ret, len * 4 + 1, 0, 0))
    kp_return (UNBOUND);

  kp_return (string::make (interp, ret));  
}

static fhandle_t std_fhandles[3];

static int
do_init_llio (interpreter *interp)
{
  HMODULE md = GetModuleHandleA ("ntdll.dll");
  if (!md)
    return (init_op::fail ("could not get a handle for ntdll.dll"));

  setfh_info = (setfh_info_fn)GetProcAddress (md, "NtSetInformationFile");
  if (!setfh_info)
    return (init_op::fail ("could not find function in ntdll.dll")); 

  std_fhandles[0] = GetStdHandle (STD_INPUT_HANDLE);
  std_fhandles[1] = GetStdHandle (STD_OUTPUT_HANDLE);
  std_fhandles[2] = GetStdHandle (STD_ERROR_HANDLE);
  interp->io_event = CreateEventW (nullptr, true, false, nullptr);

  if (!fhandle_valid_p (std_fhandles[0]) ||
      !fhandle_valid_p (std_fhandles[1]) ||
      !fhandle_valid_p (std_fhandles[2]) ||
      !fhandle_valid_p (interp->io_event))
    return (init_op::fail ("could not initialize standard handles"));

  WSADATA wsd;
  if (WSAStartup (MAKEWORD (2, 2), &wsd) != 0)
    return (init_op::fail ("could not initialize socket subsystem"));

  return (init_op::result_ok);
}

fhandle_t llio_stdfh (unsigned int idx)
{
  return (idx <= 2 ? std_fhandles[idx] : INVALID_HANDLE_VALUE);
}

KP_DECLS_END
